Table of Contents

  • 1  Обзор данных.
  • 2  Предобработка.
  • 3  Функции для расчёта и анализа LTV, ROI, удержания и конверсии.
  • 4  Исследовательский анализ данных
    • 4.1  Профили пользователей.
    • 4.2  Регионы.
    • 4.3  Устройства.
    • 4.4  Каналы привлечения.
    • 4.5  Выводы.
  • 5  Маркетинг.
  • 6  Окупаемость рекламы.
    • 6.1  По всем пользователям.
      • 6.1.1  по странам
      • 6.1.2  по устройствам
      • 6.1.3  по каналам привлечения
    • 6.2  Выводы.
  • 7  Итоги исследования.
  • 8  P.S.
    • 8.1  Пользователи из США.
    • 8.2  Пользователи из Англии.
    • 8.3  Пользователи из Франции.
    • 8.4  Пользователи из Германии.
    • 8.5  Дополнительные выводы.

Анализ эффективности рекламных вложений.¶

Несмотря на огромные вложения в рекламу развлекательного приложения Procrastinate Pro+, последние несколько месяцев компания терпит убытки. Задача — разобраться в причинах и помочь компании выйти в плюс.

Цель исследования изучить:

  • откуда приходят пользователи и какими устройствами они пользуются,
  • сколько стоит привлечение пользователей из различных рекламных каналов;
  • сколько денег приносит каждый клиент,
  • когда расходы на привлечение клиента окупаются,
  • какие факторы мешают привлечению клиентов.

Исследование пройдет в несколько этапов:

  1. Обзор данных.
  2. Предобработка.
  3. Создание функций для анализа.
  4. Исследовательский анализ профилей.
  5. Обзор маргетинговых расходов.
  6. Анализ окупаемости рекламы.

Обзор данных.¶

Есть данные о пользователях, привлечённых с 1 мая по 27 октября 2019 года:

  • visits_info_short.csv хранит лог сервера с информацией о посещениях сайта,
  • orders_info_short.csv — информацию о заказах,
  • costs_info_short.csv — информацию о расходах на рекламу.

Структура visits_info_short.csv:

  • User Id — уникальный идентификатор пользователя,
  • Region — страна пользователя,
  • Device — тип устройства пользователя,
  • Channel — идентификатор источника перехода,
  • Session Start — дата и время начала сессии,
  • Session End — дата и время окончания сессии.

Структура orders_info_short.csv:

  • User Id — уникальный идентификатор пользователя,
  • Event Dt — дата и время покупки,
  • Revenue — сумма заказа.

Структура costs_info_short.csv:

  • dt — дата проведения рекламной кампании,
  • Channel — идентификатор рекламного источника,
  • costs — расходы на эту кампанию.
In [1]:
import pandas as pd 
import numpy as np 
import os 
from datetime import datetime, timedelta
from matplotlib import pyplot as plt

pd.options.display.float_format = '{:.2f}'.format

Прочитаем файлы и сохраним их в переменной df. Предусмотрим чтение файла из локального хранилища и из облака Яндекс. Выведем на экран 10 первых строк.

In [2]:
def read_file(file, number):
    '''Функция обзора данных. 
       file - имя файла в формате 'xxxx.csv'
       number - целое число.
       Считывает файл из облака Яндекс.Практикума или из домашнего каталога с ноутбуком.
       Выводит на экран number-строк датафрейма и общую информацию о количестве строк и типах данных.   
    '''

    if os.path.exists('----' + file): # проверка наличия файла в облаке
        df = pd.read_csv('----' + file) # чтение файла с данными и сохранение
    else:
        try:
            df = pd.read_csv(file)
        except:
            display('Проверьте наличие файла на локальном хранилище!')
    
    display(df.head(number))  
    print()
    display(df.info())
    print()
    
    # подсчет уникальных значений по столбцу и вывод их на экран (если количество не превышает 20)
    for col in df.columns:
        print( col, df[col].nunique())
        print('минимальное значение', df[col].min())
        print('максимальное значение', df[col].max())
        if df[col].nunique() < 20:
            print(df[col].sort_values().unique())
        print()
    
    print('Количество дубликатов', df.duplicated().sum())
    
    shape = df.shape
    
    return df, shape
In [3]:
visits, visits_shape = read_file('----.csv', 5)
User Id Region Device Channel Session Start Session End
0 981449118918 United States iPhone organic 2019-05-01 02:36:01 2019-05-01 02:45:01
1 278965908054 United States iPhone organic 2019-05-01 04:46:31 2019-05-01 04:47:35
2 590706206550 United States Mac organic 2019-05-01 14:09:25 2019-05-01 15:32:08
3 326433527971 United States Android TipTop 2019-05-01 00:29:59 2019-05-01 00:54:25
4 349773784594 United States Mac organic 2019-05-01 03:33:35 2019-05-01 03:57:40
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 309901 entries, 0 to 309900
Data columns (total 6 columns):
 #   Column         Non-Null Count   Dtype 
---  ------         --------------   ----- 
 0   User Id        309901 non-null  int64 
 1   Region         309901 non-null  object
 2   Device         309901 non-null  object
 3   Channel        309901 non-null  object
 4   Session Start  309901 non-null  object
 5   Session End    309901 non-null  object
dtypes: int64(1), object(5)
memory usage: 14.2+ MB
None
User Id 150008
минимальное значение 599326
максимальное значение 999999563947

Region 4
минимальное значение France
максимальное значение United States
['France' 'Germany' 'UK' 'United States']

Device 4
минимальное значение Android
максимальное значение iPhone
['Android' 'Mac' 'PC' 'iPhone']

Channel 11
минимальное значение AdNonSense
максимальное значение organic
['AdNonSense' 'FaceBoom' 'LeapBob' 'MediaTornado' 'OppleCreativeMedia'
 'RocketSuperAds' 'TipTop' 'WahooNetBanner' 'YRabbit' 'lambdaMediaAds'
 'organic']

Session Start 306813
минимальное значение 2019-05-01 00:00:41
максимальное значение 2019-10-31 23:59:23

Session End 306793
минимальное значение 2019-05-01 00:07:06
максимальное значение 2019-11-01 01:38:46

Количество дубликатов 0
  • Данные о посещениях сайта не имеют пропусков.
  • Необходимо преобразование Session Start, Session End в формат datetime.
  • Наименование колонок не соответствует стилю. Необходимо привести к нижнему регистру, пробел заменить на нижнее подчеркивание.
  • Дубликатов нет.
  • Клиенты компании из 4 регионов: США, Англия, Франция и Германия.
  • Клиенты пользуются 4 устройствами: мобильное приложение на Android и iOS, десктопное приложение PC и Mac.
  • Клиенты приходят к нам из 11 каналов.
  • Данные представлены с 1 мая по 31 октября 2019.
In [4]:
orders, orders_shape = read_file('----.csv', 10)
User Id Event Dt Revenue
0 188246423999 2019-05-01 23:09:52 4.99
1 174361394180 2019-05-01 12:24:04 4.99
2 529610067795 2019-05-01 11:34:04 4.99
3 319939546352 2019-05-01 15:34:40 4.99
4 366000285810 2019-05-01 13:59:51 4.99
5 129100331057 2019-05-01 01:56:28 4.99
6 626807144131 2019-05-01 02:39:21 4.99
7 914062168985 2019-05-01 02:02:18 4.99
8 769669137453 2019-05-01 03:39:47 4.99
9 434198630691 2019-05-01 18:16:50 5.99
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 40212 entries, 0 to 40211
Data columns (total 3 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   User Id   40212 non-null  int64  
 1   Event Dt  40212 non-null  object 
 2   Revenue   40212 non-null  float64
dtypes: float64(1), int64(1), object(1)
memory usage: 942.6+ KB
None
User Id 8881
минимальное значение 599326
максимальное значение 999895427370

Event Dt 40163
минимальное значение 2019-05-01 00:28:11
максимальное значение 2019-10-31 23:56:56

Revenue 5
минимальное значение 4.99
максимальное значение 49.99
[ 4.99  5.99  9.99 19.99 49.99]

Количество дубликатов 0
  • Данные о покупках не имеют пропусков.
  • Необходимо преобразование колонки с датой Event Dt в формат datetime.
  • Требуется привести названия колонок к общепринятому стилю.
  • Дубликатов нет.
  • Данные представлены за тот же период, что и посещения, с 1 мая по 31 октября 2019г.
  • Размер покупки варьируется от 5 до 50 у.е. Всего можно выделить 5 сегментов, вероятно это фиксированные тарифные предложение: 5, 6, 10, 20 и 50 у.е.
In [5]:
costs, costs_shape = read_file('----.csv', 10)
dt Channel costs
0 2019-05-01 FaceBoom 113.30
1 2019-05-02 FaceBoom 78.10
2 2019-05-03 FaceBoom 85.80
3 2019-05-04 FaceBoom 136.40
4 2019-05-05 FaceBoom 122.10
5 2019-05-06 FaceBoom 118.80
6 2019-05-07 FaceBoom 101.20
7 2019-05-08 FaceBoom 100.10
8 2019-05-09 FaceBoom 93.50
9 2019-05-10 FaceBoom 104.50
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1800 entries, 0 to 1799
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   dt       1800 non-null   object 
 1   Channel  1800 non-null   object 
 2   costs    1800 non-null   float64
dtypes: float64(1), object(2)
memory usage: 42.3+ KB
None
dt 180
минимальное значение 2019-05-01
максимальное значение 2019-10-27

Channel 10
минимальное значение AdNonSense
максимальное значение lambdaMediaAds
['AdNonSense' 'FaceBoom' 'LeapBob' 'MediaTornado' 'OppleCreativeMedia'
 'RocketSuperAds' 'TipTop' 'WahooNetBanner' 'YRabbit' 'lambdaMediaAds']

costs 608
минимальное значение 0.8
максимальное значение 630.0

Количество дубликатов 0
  • Данные о расходах на рекламу представлены за период с 1 мая по 27 октября 2019г.
  • Пропусков в данных нет.
  • Необходимо преобразовать формат хранения даты рекламной компании dt к типу datetime.
  • Требуется привести названия столбцов к нижнему регистру.
  • Данные о расходах на рекламу представлены из 10 каналов. В то время как клиенты приходят из 11 (+ органика).

Предобработка.¶

Преобразуем колонки с датами к типу datetime. Приведем названия колонок в соответствие со стилем.

In [6]:
def column_style(df):
    '''Преобразование названий колонок. Перевод в нижний регистр, замена пробелов на нижнее подчеркивание'''
    
    df.columns = df.columns.str.lower().str.replace(' ', '_')
In [7]:
visits['Session Start'] = pd.to_datetime(visits['Session Start'])
visits['Session End'] = pd.to_datetime(visits['Session End'])
column_style(visits)
visits.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 309901 entries, 0 to 309900
Data columns (total 6 columns):
 #   Column         Non-Null Count   Dtype         
---  ------         --------------   -----         
 0   user_id        309901 non-null  int64         
 1   region         309901 non-null  object        
 2   device         309901 non-null  object        
 3   channel        309901 non-null  object        
 4   session_start  309901 non-null  datetime64[ns]
 5   session_end    309901 non-null  datetime64[ns]
dtypes: datetime64[ns](2), int64(1), object(3)
memory usage: 14.2+ MB
In [8]:
orders['Event Dt'] = pd.to_datetime(orders['Event Dt'])
column_style(orders)
orders.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 40212 entries, 0 to 40211
Data columns (total 3 columns):
 #   Column    Non-Null Count  Dtype         
---  ------    --------------  -----         
 0   user_id   40212 non-null  int64         
 1   event_dt  40212 non-null  datetime64[ns]
 2   revenue   40212 non-null  float64       
dtypes: datetime64[ns](1), float64(1), int64(1)
memory usage: 942.6 KB
In [9]:
costs['dt'] = pd.to_datetime(costs['dt'])
column_style(costs)
costs.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1800 entries, 0 to 1799
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype         
---  ------   --------------  -----         
 0   dt       1800 non-null   datetime64[ns]
 1   channel  1800 non-null   object        
 2   costs    1800 non-null   float64       
dtypes: datetime64[ns](1), float64(1), object(1)
memory usage: 42.3+ KB

Функции для расчёта и анализа LTV, ROI, удержания и конверсии.¶

In [10]:
# функция для создания пользовательских профилей

def get_profiles(sessions, orders, ad_costs, event_names=[]):

    # находим параметры первых посещений
    profiles = (
        sessions.sort_values(by=['user_id', 'session_start'])
        .groupby('user_id')
        .agg(
            {
                'session_start': 'first',
                'channel': 'first',
                'device': 'first',
                'region': 'first',
            }
        )
        .rename(columns={'session_start': 'first_ts'})
        .reset_index()
    )

    # для когортного анализа определяем дату первого посещения
    # и первый день месяца, в который это посещение произошло
    profiles['dt'] = profiles['first_ts'].astype('datetime64[D]')
    profiles['month'] = profiles['first_ts'].astype('datetime64[M]')

    # добавляем признак платящих пользователей
    profiles['payer'] = profiles['user_id'].isin(orders['user_id'].unique())


    # считаем количество уникальных пользователей
    # с одинаковыми источником и датой привлечения
    new_users = (
        profiles.groupby(['dt', 'channel'])
        .agg({'user_id': 'nunique'})
        .rename(columns={'user_id': 'unique_users'})
        .reset_index()
    )

    # объединяем траты на рекламу и число привлечённых пользователей
    ad_costs = ad_costs.merge(new_users, on=['dt', 'channel'], how='left')

    # делим рекламные расходы на число привлечённых пользователей
    ad_costs['acquisition_cost'] = ad_costs['costs'] / ad_costs['unique_users']

    # добавляем стоимость привлечения в профили
    profiles = profiles.merge(
        ad_costs[['dt', 'channel', 'acquisition_cost']],
        on=['dt', 'channel'],
        how='left',
    )

    # стоимость привлечения органических пользователей равна нулю
    profiles['acquisition_cost'] = profiles['acquisition_cost'].fillna(0)

    return profiles
In [11]:
# функция для расчёта удержания

def get_retention(
    profiles,
    sessions,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):

    # добавляем столбец payer в передаваемый dimensions список
    dimensions = ['payer'] + dimensions

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')

    # собираем «сырые» данные для расчёта удержания
    result_raw = result_raw.merge(
        sessions[['user_id', 'session_start']], on='user_id', how='left'
    )
    result_raw['lifetime'] = (
        result_raw['session_start'] - result_raw['first_ts']
    ).dt.days

    # функция для группировки таблицы по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        result = df.pivot_table(
            index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
        )
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        result = result.div(result['cohort_size'], axis=0)
        result = result[['cohort_size'] + list(range(horizon_days))]
        result['cohort_size'] = cohort_sizes
        return result

    # получаем таблицу удержания
    result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)

    # получаем таблицу динамики удержания
    result_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    # возвращаем обе таблицы и сырые данные
    return result_raw, result_grouped, result_in_time
In [12]:
# функция для расчёта конверсии

def get_conversion(
    profiles,
    purchases,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')

    # определяем дату и время первой покупки для каждого пользователя
    first_purchases = (
        purchases.sort_values(by=['user_id', 'event_dt'])
        .groupby('user_id')
        .agg({'event_dt': 'first'})
        .reset_index()
    )

    # добавляем данные о покупках в профили
    result_raw = result_raw.merge(
        first_purchases[['user_id', 'event_dt']], on='user_id', how='left'
    )

    # рассчитываем лайфтайм для каждой покупки
    result_raw['lifetime'] = (
        result_raw['event_dt'] - result_raw['first_ts']
    ).dt.days

    # группируем по cohort, если в dimensions ничего нет
    if len(dimensions) == 0:
        result_raw['cohort'] = 'All users' 
        dimensions = dimensions + ['cohort']

    # функция для группировки таблицы по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        result = df.pivot_table(
            index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
        )
        result = result.fillna(0).cumsum(axis = 1)
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        # делим каждую «ячейку» в строке на размер когорты
        # и получаем conversion rate
        result = result.div(result['cohort_size'], axis=0)
        result = result[['cohort_size'] + list(range(horizon_days))]
        result['cohort_size'] = cohort_sizes
        return result

    # получаем таблицу конверсии
    result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)

    # для таблицы динамики конверсии убираем 'cohort' из dimensions
    if 'cohort' in dimensions: 
        dimensions = []

    # получаем таблицу динамики конверсии
    result_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    # возвращаем обе таблицы и сырые данные
    return result_raw, result_grouped, result_in_time
In [13]:
# функция для расчёта LTV и ROI

def get_ltv(
    profiles,
    purchases,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
    # добавляем данные о покупках в профили
    result_raw = result_raw.merge(
        purchases[['user_id', 'event_dt', 'revenue']], on='user_id', how='left'
    )
    # рассчитываем лайфтайм пользователя для каждой покупки
    result_raw['lifetime'] = (
        result_raw['event_dt'] - result_raw['first_ts']
    ).dt.days
    # группируем по cohort, если в dimensions ничего нет
    if len(dimensions) == 0:
        result_raw['cohort'] = 'All users'
        dimensions = dimensions + ['cohort']

    # функция группировки по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        # строим «треугольную» таблицу выручки
        result = df.pivot_table(
            index=dims, columns='lifetime', values='revenue', aggfunc='sum'
        )
        # находим сумму выручки с накоплением
        result = result.fillna(0).cumsum(axis=1)
        # вычисляем размеры когорт
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        # объединяем размеры когорт и таблицу выручки
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        # считаем LTV: делим каждую «ячейку» в строке на размер когорты
        result = result.div(result['cohort_size'], axis=0)
        # исключаем все лайфтаймы, превышающие горизонт анализа
        result = result[['cohort_size'] + list(range(horizon_days))]
        # восстанавливаем размеры когорт
        result['cohort_size'] = cohort_sizes

        # собираем датафрейм с данными пользователей и значениями CAC, 
        # добавляя параметры из dimensions
        cac = df[['user_id', 'acquisition_cost'] + dims].drop_duplicates()

        # считаем средний CAC по параметрам из dimensions
        cac = (
            cac.groupby(dims)
            .agg({'acquisition_cost': 'mean'})
            .rename(columns={'acquisition_cost': 'cac'})
        )

        # считаем ROI: делим LTV на CAC
        roi = result.div(cac['cac'], axis=0)

        # удаляем строки с бесконечным ROI
        roi = roi[~roi['cohort_size'].isin([np.inf])]

        # восстанавливаем размеры когорт в таблице ROI
        roi['cohort_size'] = cohort_sizes

        # добавляем CAC в таблицу ROI
        roi['cac'] = cac['cac']

        # в финальной таблице оставляем размеры когорт, CAC
        # и ROI в лайфтаймы, не превышающие горизонт анализа
        roi = roi[['cohort_size', 'cac'] + list(range(horizon_days))]

        # возвращаем таблицы LTV и ROI
        return result, roi

    # получаем таблицы LTV и ROI
    result_grouped, roi_grouped = group_by_dimensions(
        result_raw, dimensions, horizon_days
    )

    # для таблиц динамики убираем 'cohort' из dimensions
    if 'cohort' in dimensions:
        dimensions = []

    # получаем таблицы динамики LTV и ROI
    result_in_time, roi_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    return (
        result_raw,  # сырые данные
        result_grouped,  # таблица LTV
        result_in_time,  # таблица динамики LTV
        roi_grouped,  # таблица ROI
        roi_in_time,  # таблица динамики ROI
    )
In [14]:
# функция для сглаживания фрейма

def filter_data(df, window):
    # для каждого столбца применяем скользящее среднее
    for column in df.columns.values:
        df[column] = df[column].rolling(window).mean() 
    return df
In [15]:
# функция для визуализации удержания

def plot_retention(retention, retention_history, horizon, window=7):

    # задаём размер сетки для графиков
    plt.figure(figsize=(15, 10))

    # исключаем размеры когорт и удержание первого дня
    retention = retention.drop(columns=['cohort_size', 0])
    # в таблице динамики оставляем только нужный лайфтайм
    retention_history = retention_history.drop(columns=['cohort_size'])[
        [horizon - 1]
    ]

    # если в индексах таблицы удержания только payer,
    # добавляем второй признак — cohort
    if retention.index.nlevels == 1:
        retention['cohort'] = 'All users'
        retention = retention.reset_index().set_index(['cohort', 'payer'])

    # в таблице графиков — два столбца и две строки, четыре ячейки
    # в первой строим кривые удержания платящих пользователей
    ax1 = plt.subplot(2, 2, 1)
    retention.query('payer == True').droplevel('payer').T.plot(
        grid=True, ax=ax1
    )
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('Удержание платящих пользователей')

    # во второй ячейке строим кривые удержания неплатящих
    # вертикальная ось — от графика из первой ячейки
    ax2 = plt.subplot(2, 2, 2, sharey=ax1)
    retention.query('payer == False').droplevel('payer').T.plot(
        grid=True, ax=ax2
    )
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('Удержание неплатящих пользователей')

    # в третьей ячейке — динамика удержания платящих
    ax3 = plt.subplot(2, 2, 3)
    # получаем названия столбцов для сводной таблицы
    columns = [
        name
        for name in retention_history.index.names
        if name not in ['dt', 'payer']
    ]
    # фильтруем данные и строим график
    filtered_data = retention_history.query('payer == True').pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax3)
    plt.xlabel('Дата привлечения')
    plt.title(
        'Динамика удержания платящих пользователей на {}-й день'.format(
            horizon
        )
    )

    # в чётвертой ячейке — динамика удержания неплатящих
    ax4 = plt.subplot(2, 2, 4, sharey=ax3)
    # фильтруем данные и строим график
    filtered_data = retention_history.query('payer == False').pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax4)
    plt.xlabel('Дата привлечения')
    plt.title(
        'Динамика удержания неплатящих пользователей на {}-й день'.format(
            horizon
        )
    )
    
    plt.tight_layout()
    plt.show()
In [16]:
# функция для визуализации конверсии

def plot_conversion(conversion, conversion_history, horizon, window=7):

    # задаём размер сетки для графиков
    plt.figure(figsize=(15, 5))

    # исключаем размеры когорт
    conversion = conversion.drop(columns=['cohort_size'])
    # в таблице динамики оставляем только нужный лайфтайм
    conversion_history = conversion_history.drop(columns=['cohort_size'])[
        [horizon - 1]
    ]

    # первый график — кривые конверсии
    ax1 = plt.subplot(1, 2, 1)
    conversion.T.plot(grid=True, ax=ax1)
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('Конверсия пользователей')

    # второй график — динамика конверсии
    ax2 = plt.subplot(1, 2, 2, sharey=ax1)
    columns = [
        # столбцами сводной таблицы станут все столбцы индекса, кроме даты
        name for name in conversion_history.index.names if name not in ['dt']
    ]
    filtered_data = conversion_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax2)
    plt.xlabel('Дата привлечения')
    plt.title('Динамика конверсии пользователей на {}-й день'.format(horizon))

    plt.tight_layout()
    plt.show()
In [17]:
# функция для визуализации LTV и ROI

def plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon, window=7):

    # задаём сетку отрисовки графиков
    plt.figure(figsize=(20, 10))

    # из таблицы ltv исключаем размеры когорт
    ltv = ltv.drop(columns=['cohort_size'])
    # в таблице динамики ltv оставляем только нужный лайфтайм
    ltv_history = ltv_history.drop(columns=['cohort_size'])[[horizon - 1]]

    # стоимость привлечения запишем в отдельный фрейм
    cac_history = roi_history[['cac']]

    # из таблицы roi исключаем размеры когорт и cac
    roi = roi.drop(columns=['cohort_size', 'cac'])
    # в таблице динамики roi оставляем только нужный лайфтайм
    roi_history = roi_history.drop(columns=['cohort_size', 'cac'])[
        [horizon - 1]
    ]

    # первый график — кривые ltv
    ax1 = plt.subplot(2, 3, 1)
    ltv.T.plot(grid=True, ax=ax1)
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('LTV')

    # второй график — динамика ltv
    ax2 = plt.subplot(2, 3, 2, sharey=ax1)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in ltv_history.index.names if name not in ['dt']]
    filtered_data = ltv_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax2)
    plt.xlabel('Дата привлечения')
    plt.title('Динамика LTV пользователей на {}-й день'.format(horizon))

    # третий график — динамика cac
    ax3 = plt.subplot(2, 3, 3, sharey=ax1)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in cac_history.index.names if name not in ['dt']]
    filtered_data = cac_history.pivot_table(
        index='dt', columns=columns, values='cac', aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax3)
    plt.xlabel('Дата привлечения')
    plt.title('Динамика стоимости привлечения пользователей')

    # четвёртый график — кривые roi
    ax4 = plt.subplot(2, 3, 4)
    roi.T.plot(grid=True, ax=ax4)
    plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.title('ROI')

    # пятый график — динамика roi
    ax5 = plt.subplot(2, 3, 5, sharey=ax4)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in roi_history.index.names if name not in ['dt']]
    filtered_data = roi_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax5)
    plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
    plt.xlabel('Дата привлечения')
    plt.title('Динамика ROI пользователей на {}-й день'.format(horizon))

    plt.tight_layout()
    plt.show()

Напишем функцию для анализа профилей пользователей по выбранным критериям.

In [18]:
def users_analytic(df, column):
    '''Анализирует пользователей по выбранному признаку. С учетом платящий/неплатящий.
    Определяет долю платящих пользователей внутри групп и конверсию в платящего.
    '''
# Группировка данных по выбранному признаку, подсчет пользователей в каждой группе и их доли.
    users_sum = df.groupby([column]).agg({'user_id' : 'count'}).reset_index()
    users_sum['%_total'] = users_sum['user_id'] / users_sum['user_id'].sum() * 100
    
# Группировка данных по выбранному признаку, подсчет платящих пользователей в каждой группе и их доли
    users_payer_analytic = df.query('payer == True').groupby(column).agg({'user_id' : 'count'})\
    .reset_index().rename(columns={'user_id':'payers'})
    users_payer_analytic['%_payers'] = users_payer_analytic['payers'] / users_payer_analytic['payers'].sum() * 100

# объединение данных по всем пользователям и платящим, сортировка по количеству платящих клиентов
    users_payer_analytic = users_sum.merge(users_payer_analytic, on=column)\
    .rename(columns={'user_id':'users_total'})\
    .sort_values(by='payers', ascending=False)

# рассчет конверсиии в платящего   
    users_payer_analytic['conversion'] = users_payer_analytic['payers'] / users_payer_analytic['users_total'] * 100
    
    display('Структура платящих пользователей', users_payer_analytic)
    
# рассчет рекламного бюджета
    market_budget = df.groupby(column).agg({'acquisition_cost':'sum'})\
    .reset_index().rename(columns={'acquisition_cost':'market_budget'})\
    .sort_values(by='market_budget', ascending=False)
    market_budget['%'] = market_budget['market_budget'] / market_budget['market_budget'].sum() * 100
    display('Рекламный бюджет', market_budget)
    

Исследовательский анализ данных¶

Профили пользователей.¶

Получим профили пользователей.

In [19]:
profiles = get_profiles(visits, orders, costs)
print(profiles.head(5)) 
    user_id            first_ts     channel  device         region         dt  \
0    599326 2019-05-07 20:58:57    FaceBoom     Mac  United States 2019-05-07   
1   4919697 2019-07-09 12:46:07    FaceBoom  iPhone  United States 2019-07-09   
2   6085896 2019-10-01 09:58:33     organic  iPhone         France 2019-10-01   
3  22593348 2019-08-22 21:35:48  AdNonSense      PC        Germany 2019-08-22   
4  31989216 2019-10-02 00:07:44     YRabbit  iPhone  United States 2019-10-02   

       month  payer  acquisition_cost  
0 2019-05-01   True              1.09  
1 2019-07-01  False              1.11  
2 2019-10-01  False              0.00  
3 2019-08-01  False              0.99  
4 2019-10-01  False              0.23  

Определим минимальную и максимальную даты привлечения пользователей.

In [20]:
min_user_date = profiles['dt'].min()
max_user_date = profiles['dt'].max()
print(min_user_date)
print(max_user_date)
2019-05-01 00:00:00
2019-10-27 00:00:00

Регионы.¶

In [21]:
users_analytic(profiles, 'region')
'Структура платящих пользователей'
region users_total %_total payers %_payers conversion
3 United States 100002 66.66 6902 77.72 6.90
2 UK 17575 11.72 700 7.88 3.98
0 France 17450 11.63 663 7.47 3.80
1 Germany 14981 9.99 616 6.94 4.11
'Рекламный бюджет'
region market_budget %
3 United States 90928.60 86.19
0 France 5075.97 4.81
2 UK 5068.50 4.80
1 Germany 4424.22 4.19
  • Пользователи приложения проживают в 4 странах:
    • 66% в США,
    • по 12% в Англии и Франции,
    • 10% в Германии.
  • Конверсия в платящих пользователей максимальна для США 6.94% и минимальна для Франции 3.8%
  • Среди платящих пользоваателей на США приходится 78%, доля остальных стран около 7-8%.
  • Рекламный бюджет сосредоточен на пользователях из США.

Устройства.¶

In [22]:
users_analytic(profiles, 'device')
'Структура платящих пользователей'
device users_total %_total payers %_payers conversion
3 iPhone 54479 36.32 3382 38.08 6.21
0 Android 35032 23.35 2050 23.08 5.85
1 Mac 30042 20.03 1912 21.53 6.36
2 PC 30455 20.30 1537 17.31 5.05
'Рекламный бюджет'
device market_budget %
3 iPhone 43678.79 41.40
1 Mac 24094.54 22.84
0 Android 22460.63 21.29
2 PC 15263.34 14.47
  • 60% клиентов пользуются продуктом через мобильное приложение (36% с Phone, 24% с Android), 40% через десктопную версию в равной мере PC и Mac.
  • 38% платящих пользователей пользуются iPhone, 23% Android.
  • Лучше всего конвертируются в платящих пользователи Maс - 6.36% стоит обратить на них внимание, так как их доля среди клиентов минимальна 20.03%
  • 60% рекламного бюджета расходуется на привлечение пользователей мобильных приложений. 40% на пользователей iPhone.

Каналы привлечения.¶

In [23]:
users_analytic(profiles, 'channel')
'Структура платящих пользователей'
channel users_total %_total payers %_payers conversion
1 FaceBoom 29144 19.43 3557 40.05 12.20
6 TipTop 19561 13.04 1878 21.15 9.60
10 organic 56439 37.62 1160 13.06 2.06
7 WahooNetBanner 8553 5.70 453 5.10 5.30
0 AdNonSense 3880 2.59 440 4.95 11.34
5 RocketSuperAds 4448 2.97 352 3.96 7.91
2 LeapBob 8553 5.70 262 2.95 3.06
4 OppleCreativeMedia 8605 5.74 233 2.62 2.71
9 lambdaMediaAds 2149 1.43 225 2.53 10.47
8 YRabbit 4312 2.87 165 1.86 3.83
3 MediaTornado 4364 2.91 156 1.76 3.57
'Рекламный бюджет'
channel market_budget %
6 TipTop 54751.30 51.90
1 FaceBoom 32445.60 30.75
7 WahooNetBanner 5151.00 4.88
0 AdNonSense 3911.25 3.71
4 OppleCreativeMedia 2151.25 2.04
5 RocketSuperAds 1833.00 1.74
2 LeapBob 1797.60 1.70
9 lambdaMediaAds 1557.60 1.48
3 MediaTornado 954.48 0.90
8 YRabbit 944.22 0.90
10 organic 0.00 0.00
  • 37.62% пользователей органические, при этом их конверсия в платящих минимальна 2.06%, но дает 13.06 платящих пользователей.
  • 19.43% приходят из FaceBoom, 13.04 из TipTop. FaceBoom приносит нам 40.05% платящих пользователей (конверсия для данного канала максимальна 12.2%). Из TipTop приходит 21.15% платящих пользователей (конверсия составляет 9.6%).
  • 80% рекламного бюджета расходуется на каналы FaceBoom и TipTop.
  • TipTop потребляет 52% рекламного бюджета и приносит 21% платящих пользователей. В то время как FaceBoom при тратах в 31% рекламного бюджета поставляет 40% платящих пользователей. Стоит дополнительно проанализировать эффективность структуры распределения рекламного бюджета.
  • Доля остальных каналов привлечения составляет 1,43-5.7%. Стоит обратить внимание на два канала AdNonSense с конверсией 11.34% и lambdaMediaAds с конверсией в 10.47%.

Выводы.¶

66% пользователей продукта проживают в США, по 12% в Англии и Франции, 10% в Германии. Среди платящих пользоваателей на США приходится 78%, доля остальных стран около 7-8%. 86% рекламного бюджета расходуется на привлечение пользователей из США.

60% клиентов пользуются продуктом через мобильное приложение (36% с Phone, 24% с Android), 40% через десктопную версию в равной мере PC и Mac. Лучше всего конвертируются в платящих пользователи Maс - 6.36% стоит обратить на них внимание, так как их доля среди клиентов минимальна 20.03%

37.62% пользователей органические, их конверсия в платящих минимальна 2.06% (13.06 платящих пользователей). 19.43% приходят из FaceBoom и стоят нам 31% рекламного бюджета. Максимальная для каналов конверсия в 12.2% дает 40.05% платящих пользователей. 13.04% приходят из TipTop и стоят нам 52% рекламного бюджета. Данный канал приводит 21.15% платящих пользователей. Доля остальных каналов привлечения составляет 1,43-5.7%. Из них высокая конверсия у AdNonSense - 11.34% и lambdaMediaAds - 10.47%.

Маркетинг.¶

In [24]:
costs.head()
Out[24]:
dt channel costs
0 2019-05-01 FaceBoom 113.30
1 2019-05-02 FaceBoom 78.10
2 2019-05-03 FaceBoom 85.80
3 2019-05-04 FaceBoom 136.40
4 2019-05-05 FaceBoom 122.10

Посчитаем общую сумму расходов на маркетинг.

In [25]:
costs['costs'].sum()
Out[25]:
105497.30000000002

Посчитаем расходы на маркетинг в разрезе каналов привлечения.

In [26]:
costs.groupby('channel').agg({'costs':'sum'}).sort_values(by='costs').reset_index()
Out[26]:
channel costs
0 YRabbit 944.22
1 MediaTornado 954.48
2 lambdaMediaAds 1557.60
3 LeapBob 1797.60
4 RocketSuperAds 1833.00
5 OppleCreativeMedia 2151.25
6 AdNonSense 3911.25
7 WahooNetBanner 5151.00
8 FaceBoom 32445.60
9 TipTop 54751.30

Построим визуализацию динамики изменения расходов во времени (по неделям и месяцам) по каждому источнику.

In [27]:
filter_data(costs.pivot_table(index='dt', columns='channel', values='costs', aggfunc='sum'), 4).plot(
        grid=True, figsize=(20, 15))
plt.xlabel('Период')
plt.title('Динамика расходов на маркетинг', fontsize = 15)
Out[27]:
Text(0.5, 1.0, 'Динамика расходов на маркетинг')

Исключим два самых дорогих канала привлечения.

In [28]:
filter_data(costs.pivot_table(index='dt', columns='channel', values='costs', aggfunc='sum')\
.drop(columns=['TipTop', 'FaceBoom']), 7).plot(
        grid=True, figsize=(20, 15))
plt.xlabel('Период')
plt.title('Динамика расходов на маркетинг')
Out[28]:
Text(0.5, 1.0, 'Динамика расходов на маркетинг')
  • В расходах на рекламу выделяются два канала TipTop и FaceBoom, они же лидеры по привлечению платящих пользователй. Реклама здесь в 10 раз дороже, чем на других каналах. Стоимость рекламы на этих каналах за исследуемый период устойчиво растет в TipTop с 80 до 500, в FaceBoom со 100 до 250. Необходимо проверить, окупаются ли такие затраты.
  • Расходы на канал AdNonSense снизились с 40 до 20 после первого месяца использования и стабильны.
  • Аналогично снизились расходы по каналам RocketSuperAds, lambdaMediaAds, YRabbit, MediaTornado.
  • Увеличились расходы после первого месяца использования по каналам WahooNetBanner, LeapBob, OppleCreativeMedia.
  • Вцелом динамика расходов симметрична для всех каналов. Вероятно анализ эффективности вложений был проведен по результатам первого месяца использования и далее не корректировался.

Узнаем, сколько в среднем стоило привлечение одного пользователя (CAC) из каждого источника.

In [29]:
cac = profiles.groupby('channel')\
.agg({'acquisition_cost':'sum', 'user_id': 'nunique', 'payer':'sum'}).reset_index()
cac['cac_user'] = cac['acquisition_cost'] / cac['user_id'] 
cac['cac_payer'] = cac['acquisition_cost'] / cac['payer'] 
cac.sort_values(by='cac_payer')
Out[29]:
channel acquisition_cost user_id payer cac_user cac_payer
10 organic 0.00 56439 1160 0.00 0.00
5 RocketSuperAds 1833.00 4448 352 0.41 5.21
8 YRabbit 944.22 4312 165 0.22 5.72
3 MediaTornado 954.48 4364 156 0.22 6.12
2 LeapBob 1797.60 8553 262 0.21 6.86
9 lambdaMediaAds 1557.60 2149 225 0.72 6.92
0 AdNonSense 3911.25 3880 440 1.01 8.89
1 FaceBoom 32445.60 29144 3557 1.11 9.12
4 OppleCreativeMedia 2151.25 8605 233 0.25 9.23
7 WahooNetBanner 5151.00 8553 453 0.60 11.37
6 TipTop 54751.30 19561 1878 2.80 29.15
  • Всреднем привлечение 1 пользователя обошлось нам в 0.21-2.8 доллара. Самый дорогой канал TipTop - 2.8, далее следует FaceBoom - 1.11 (в 3 раза дешевле и приносит в 2 раза больше платящих пользователей).
  • Наиболее дешево обошлись пользователи с LeapBob, MediaTornado и YRabbit. С этих же каналов приходят наименее дорогие платящие пользователи.
  • Стоимость привлечения платящих пользователей варьируется от 5.21 на RocketSuperAds до 29.15 на TipTop.

Окупаемость рекламы.¶

  • Момент анализа 1 ноября 2019 года
  • Горизонт анализа - 14 дней. Пользователи должны окупаться не позднее чем через две недели после привлечения.

По всем пользователям.¶

Проанализируем окупаемость рекламы c помощью графиков LTV и ROI, а также графиков динамики LTV, CAC и ROI. Исключим из анализа органических пользователей, так как расходы на рекламу влияют на них косвенно.

In [30]:
horizon_days = 14
observation_date = datetime(2019, 11, 1).date()

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles.query('channel != "organic"'),
    orders,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
)

plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days) 
  • Расходы на маркетинг не окупаются за 14 дней, ROI едва превысил 80%.
  • Бюджет на рекламу растет. САС резко возростает с июня и стабилизировался лишь в октябре. Майские пользователи стоили нам 0.7 доллара, сентябрьские 1.3.
  • LTV пользователей подвержен сезонности, но достаточно стабилен.
  • Окупаемость пользователей снижается. Пользователи, привлекаемые в мае, окупались, а начиная с июня (несмотря на увеличение рекламного бюджета) уже нет. Мы привлекаем все более дорогих пользователей, которые не приносят нам больше денег (качество пользователей стабильно).

Посмотрим на удержание и конверсию пользователей.

In [31]:
# смотрим конверсию

conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles.query('channel != "organic"'), 
    orders, observation_date, horizon_days, dimensions=[]
)

plot_conversion(conversion_grouped, conversion_history, horizon_days)

Привлекаемые пользователи стабильно конвертируются в платящих.

In [32]:
# смотрим удержание 

retention_raw, retention_grouped, retention_history = get_retention(
    profiles.query('channel != "organic"'), 
    visits, observation_date, horizon_days, dimensions=[]
)

plot_retention(retention_grouped, retention_history, horizon_days)
  • Удержание платящих пользователей резко падает c 35% в первый день, до 10% на 14 день. Динамика удержания относительно стабильна.
  • Необходимо посмотреть на доступные характеристики пользователей (регион, устройство, канал привлечения) и выявить, влияютли они на окупаемость и удержание.

по странам¶

In [33]:
# смотрим окупаемость с разбивкой по странам

dimensions = ['region']

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles.query('channel != "organic"'), orders, observation_date, horizon_days, dimensions=dimensions
)

plot_ltv_roi(
    ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
In [34]:
# посчитаем месячный рекламный бюджет в разрезе стран
profiles.pivot_table(index='region', columns='month', values='acquisition_cost', aggfunc='sum')
Out[34]:
month 2019-05-01 2019-06-01 2019-07-01 2019-08-01 2019-09-01 2019-10-01
region
France 801.03 850.72 793.62 953.21 877.13 800.26
Germany 727.20 679.52 675.33 797.81 801.25 743.10
UK 828.69 813.19 785.68 898.04 899.65 843.25
United States 7621.64 12746.61 14192.43 18008.81 19863.31 18495.80
  • Пользователи из Франции, Германии и Англии окупаются на 4-6 день. Проблема с пользователями из США. Они составляют 66% нашей аудитории и не окупаются за 14 дней.
  • Майские пользователи из США окупились, с июня пользователи не окупаются.
  • САС пользователей из США в 3-4 раза отличается от остальных стран. САС пользователей из США резко возросла в июне, а САС пользователей из других стран снизилась.
  • LTV пользователей достаточно стабилен, при этом пользователи из США незначительно выделяются на графиках (ожидаем, что 66% пользователей будут приносить значительно больше дохода, чем группы по 10-12%).
  • Увеличения рекламного бюджета на привлечение пользователей из США в июне значительно повлияло на САС. В то время как относительно стабильные расходы на рекламу в других регионах продолжают привлекать качественных пользователей и даже улучшили свои показатели в июне. Стоит выяснить, что повлияло на уменьшение САС пользователей не из США.
In [35]:
# смотрим конверсию с разбивкой по странам

conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles.query('channel != "organic"'), orders, observation_date, horizon_days, dimensions=dimensions
)

plot_conversion(conversion_grouped, conversion_history, horizon_days)

Пользователи из США стабильно конвертируются в платящих, причем почти в 2 раза лучше пользователей из других стран.

In [36]:
# смотрим удержание с разбивкой по странам

retention_raw, retention_grouped, retention_history = get_retention(
    profiles.query('channel != "organic"'), visits, observation_date, horizon_days, dimensions=dimensions
)

plot_retention(retention_grouped, retention_history, horizon_days)

Удержание пользователей из США самое слабое, причем оно стабильно низкое для всего периода. Стоит работать в направлении удержания пользователей из самой крупной группы.

по устройствам¶

In [37]:
# смотрим окупаемость с разбивкой по устройствам

dimensions = ['device']

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles.query('channel != "organic"'), orders, observation_date, horizon_days, dimensions=dimensions
)

plot_ltv_roi(
    ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
)
  • За 14 дней окупаются только пользователи на РС. Это самая малочисленная группа наших пользователей 17.3% и самая дешевая по стоимости привлечения. Но пользователи привлеченные с сентября не окупаются даже в этой группе.
  • Стоимость привлечения пользователей растет от месяца к месяцу, а окупаемость их падает.
  • LTV стабилен и вцелом не зависит от устройства.
In [38]:
# смотрим конверсию с разбивкой по устройствам

conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles.query('channel != "organic"'), orders, observation_date, horizon_days, dimensions=dimensions
)

plot_conversion(conversion_grouped, conversion_history, horizon_days)

Лучше всего конвертируются пользователи Mac и iPhone. У пользователей РС самая низкая и нестабильная конвертация.

In [39]:
# смотрим удержание с разбивкой по устройствам

retention_raw, retention_grouped, retention_history = get_retention(
    profiles.query('channel != "organic"'), visits, observation_date, horizon_days, dimensions=dimensions
)

plot_retention(retention_grouped, retention_history, horizon_days)

Удержание для пользователей Mac и iPhone ниже, чем для пользователей РС и Android.

по каналам привлечения¶

In [40]:
# смотрим окупаемость с разбивкой по каналам

dimensions = ['channel']

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles.query('channel != "organic"'), orders, observation_date, horizon_days, dimensions=dimensions
)

plot_ltv_roi(
    ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=20
)
In [41]:
# посчитаем месячный рекламный бюджет в разрезе каналов
profiles.pivot_table(index='channel', columns='month', values='acquisition_cost', aggfunc='sum')
Out[41]:
month 2019-05-01 2019-06-01 2019-07-01 2019-08-01 2019-09-01 2019-10-01
channel
AdNonSense 1169.70 538.65 504.00 579.60 581.70 537.60
FaceBoom 3524.40 5501.10 5294.30 6274.40 6114.90 5736.50
LeapBob 140.28 314.58 313.53 369.81 343.98 315.42
MediaTornado 238.56 138.00 138.48 154.56 144.72 140.16
OppleCreativeMedia 169.75 370.00 366.50 439.25 427.75 378.00
RocketSuperAds 577.98 325.72 252.07 253.11 218.40 205.72
TipTop 2981.00 6675.60 8410.20 11202.00 13232.50 12250.00
WahooNetBanner 418.80 921.00 851.40 1040.40 977.40 942.00
YRabbit 299.70 106.20 97.38 124.74 152.79 163.41
lambdaMediaAds 458.40 199.20 219.20 220.00 247.20 213.60
organic 0.00 0.00 0.00 0.00 0.00 0.00
  • Пользователи привлекаемые в каналах TipTop, FaceBoom и AdNonSense не окупаются за 14 дней.
  • Пользователи из TipTop окупались только в мае.
  • САС пользователей стабилен для всех каналов за исключением RocketSuperAds (снижается) и TipToр (стремительно возрастает).
  • Реклама в TipTop требует более детального анализа. Достаточно крупный канал (привлекает 20% платящих пользователей), стабильно не окупается, а мы регулярно увеличиваем вложения в него и его доля в структуре рекламного бюджета 50%.
  • Аналогична ситуация с FaceBoom. При доли в рекламном бюджете в 30%, мы получаем 40% платящих пользователей, которые регулярно не окупаются.
  • Стоит рассмотреть каналы YRabbit, lambdaMediaAds, MediaTornado и RocketSuperAds как более перспективные для вложений. Пользователи из них регулярно окупаются. Стоимость привлечения стабильно низкая. А конверсия в платящих 10.47 для lambdaMediaAds и 7.91 у RocketSuperAds.
In [42]:
# смотрим конверсию с разбивкой по каналам

conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles.query('channel != "organic"'), orders, observation_date, horizon_days, dimensions=dimensions
)

plot_conversion(conversion_grouped, conversion_history, horizon_days)

Пользователи со всех каналов конвертируются. Самая высокая конверсия у пользователей с неокупаемых каналов TipTop, FaceBoom и AdNonSense, за исключением lambdaMediaAds.

In [43]:
# смотрим удержание с разбивкой по устройствам

retention_raw, retention_grouped, retention_history = get_retention(
    profiles.query('channel != "organic"'), visits, observation_date, horizon_days, dimensions=dimensions
)

plot_retention(retention_grouped, retention_history, horizon_days)

Пользователи, привлеченные на FaceBoom и AdNonSense очень плохо удерживаются.

Выводы.¶

  • Расходы на маркетинг не окупаются за 14 дней, ROI едва превысил 80%.
  • САС за 6 месяцев вырос с 0.7 доллара до 1.3.
  • LTV пользователей подвержен сезонности, но достаточно стабилен.
  • ROI пользователей снижается. Окупилось привлечение только майских пользователей.
  • Конверсия - 8%.
  • Удержание платящих пользователей резко падает c 35% в первый день, до 10% на 14 день. Динамика удержания относительно стабильна.

Регион:

  • Пользователи из Франции, Германии и Англии окупаются на 4-6 день. Пользователи из США составляют 66% привлеченной аудитории и не окупаются за 14 дней (исключение - майские пользователи).
  • САС пользователей из США в 3-4 раза превышает CAC остальных стран. Увеличения рекламного бюджета на привлечение пользователей из США в июне значительно повлияло на САС. В то время как относительно стабильные расходы на рекламу в других регионах продолжают привлекать качественных пользователей и даже улучшили свои показатели в июне.
  • LTV пользователей достаточно стабилен, при этом пользователи из США незначительно выделяются на графиках (ожидаем, что 66% пользователей будут приносить значительно больше дохода, чем группы по 10-12%).
  • Пользователи из США стабильно конвертируются в платящих: почти 10% к 14 дню, что в 2 раза лучше пользователей из других стран (5%).
  • Удержание пользователей из США самое слабое, оно стабильно низкое для всего периода 0.35-0.1.

Стоит работать в направлении удержания пользователей из самой крупной группы.

Окупаемость рекламных вложений не зависит от типа устройства.

  • Вцелом окупились только пользователи на РС - самая малочисленная группа наших пользователей 17.3% и самая дешевая по стоимости привлечения.
  • LTV стабилен и вцелом не зависит от устройства.
  • Лучше всего конвертируются пользователи Mac и iPhone. У пользователей РС самая низкая и нестабильная конвертация.
  • Удержание для пользователей Mac и iPhone ниже, чем для пользователей РС и Android.

Стоит обратить внимание на продукт и рекламу для клиентов Mac и iPhone. У них высокая конверсия, но низкое удержание. Возможно у нас проблемы с системой iOS или мы не удовлетворяем специфических ожиданий пользователей.

Рекламные каналы разделены по региональному признаку.

  • В США мы привлекаем пользователей через TipTop, FaceBoom, YRabbit, MediaTornado и RocketSuperAds.
    • TipTop привлекает 20% платящих пользователей, потребляя 50% рекламного бюджета (мы регулярно увеличиваем вложения в него). Пользователи окупались только в мае. САС стремительно возрастает.
    • FaceBoom приводит 40% платящих пользователей, потребляя 30% рекламного бюджета.Не окупаются за 14 дней.
    • Самая высокая конверсия у пользователей с неокупаемых каналов TipTop, FaceBoom.
    • Удержание пользователей, привлеченные на FaceBoom, к 14 дню близко к 0% против 2% у остальных каналов.
    • YRabbit, MediaTornado и RocketSuperAds. Пользователи из них регулярно окупаются. Стоимость привлечения стабильно низкая. RocketSuperAds дает высокую конверсию в 7.91% и низкую САС 0.5-0.3 доллара.

80% рекламного бюджета тратится на привлечение неокупаемых пользователей через TipTop, FaceBoom. Стоит оценить целесообразность дальнейшего сотрудничества с данными каналами. Пользователей с FaceBoom мы не можем удержать, а цены TipTop непомерно высоки. RocketSuperAds выглядит перспективным для расширения использования.

  • В странах Европейского региона мы используем AdNonSense, LeapBob, OppleCreativeMedia, WahooNetBanner и lambdaMediaAds.
    • Пользователи, привлеченные в AdNonSence, не окупаются. При этом они хорошо конвертируются 11.34%, но их удержание близко к 0 на 14 лайфтайм.
    • Лучше всего окупаются пользователи с канала lambdaMediaAds. Но пользователи с этого канала приходят не регулярно. Конверсия составляет 10.47%
    • Каналы LeapBob и OppleCreativeMedia показывают наиболее стабильные результаты: минимальный САС на уровне 2,5 долларов и регулярный ROI >1.

Стоит дополнительно изучить аудиторию AdNonSence: почему она хорошо конвертируется и плохо удерживается. Возможно это клиенты, чье ожидание не соответствуют нашему продукту? Или в рекламе на этом канале мы даем неправильное понятие о содержании нашего продукта? Или это неплатежеспособная аудитория?

Итоги исследования.¶

Мы исследовали данные о пользовательской активности в приложении Procrastinate Pro+ и расходах на их привлечение за период с 1 мая по 27 октября 2019 года.

Обзор и предобработка показали, что данные "чистые". Мы преобразовали колонки с датами в формат datetime и привели наименование колонок в соответствие со стилем.

  • 66% пользователей продукта проживают в США, по 12% в Англии и Франции, 10% в Германии. Среди платящих пользоваателей на США приходится 78%, доля остальных стран около 7-8%. 86% рекламного бюджета расходуется на привлечение пользователей из США.
  • 60% клиентов пользуются продуктом через мобильное приложение (36% с Phone, 24% с Android), 40% через десктопную версию в равной мере PC и Mac.
  • 37.62% пользователей органические, их конверсия в платящих 2.06% (13.06 платящих пользователей).
  • Расходы на маркетинг не окупаются за 14 дней, ROI едва превысил 80%. САС за 6 месяцев вырос с 0.7 доллара до 1.3. LTV пользователей подвержен сезонности, но достаточно стабилен. ROI пользователей снижается. Окупилось привлечение только майских пользователей. Конверсия - 8%. Удержание платящих пользователей резко падает c 35% в первый день, до 10% на 14 день.

Пользователи из Франции, Германии и Англии окупаются на 4-6 день. Пользователи из США не окупаются. САС пользователей из США в 3-4 раза превышает CAC остальных стран 0.8-1.8 против 0.4-0.6. LTV пользователей достаточно стабилен, при этом пользователи из США незначительно выделяются на графиках (ожидаем, что 66% пользователей будут приносить значительно больше дохода, чем группы по 10-12%). Пользователи из США стабильно конвертируются в платящих: почти 10% к 14 дню, что в 2 раза лучше пользователей из других стран (5%). Удержание пользователей из США самое слабое, оно стабильно низкое для всего периода 0.35-0.1.

Окупаемость рекламных вложений не зависит от типа устройства: в США не окупаются пользователи всех устройств, в Европейском регионе - окупаются. Вцелом окупились только пользователи на РС - самая малочисленная группа наших пользователей 17.3% и самая дешевая по стоимости привлечения, при этом у них самая низкая и нестабильная конверсия.

Всреднем привлечение 1 пользователя обошлось нам в 0.21-2.8 доллара. Самый дорогой канал TipTop - 2.8. Наиболее дешево обошлись пользователи с LeapBob, MediaTornado и YRabbit. Стоимость привлечения платящих пользователей варьируется от 5.21 на RocketSuperAds до 29.15 на TipTop.

Рекламные каналы разделены по региональному признаку.

  • В США мы привлекаем пользователей через TipTop, FaceBoom, YRabbit, MediaTornado и RocketSuperAds.
    • TipTop и FaceBoom потребляют 80% рекламного бюджета, приводят 60% платящих пользователей. Не окупаются за 14 дней, так как стоимость рекламы на TipTop, FaceBoom в 10 раз дороже, чем на других каналах, а удержание пользователей, привлеченных на FaceBoom, к 14 дню близко к 0% против 2% у остальных каналов.
    • YRabbit, MediaTornado и RocketSuperAds. Пользователи из них регулярно окупаются. Стоимость привлечения стабильно низкая. RocketSuperAds дает высокую конверсию в 7.91% и низкую САС 0.5-0.3 доллара.
  • В странах Европейского региона мы используем AdNonSense, LeapBob, OppleCreativeMedia, WahooNetBanner и lambdaMediaAds.

    • Пользователи, привлеченные в AdNonSence, не окупаются. При этом они хорошо конвертируются 11.34%, но их удержание близко к 0 на 14 лайфтайм.
    • Лучше всего окупаются пользователи с канала lambdaMediaAds. Но пользователи с этого канала приходят не регулярно. Конверсия составляет 10.47%
    • Каналы LeapBob и OppleCreativeMedia показывают наиболее стабильные результаты: минимальный САС на уровне 2,5 долларов и регулярный ROI >1.
    • WahooNetBanner имеет 36% в структуре рекламных расходов по Англии. Последние два месяца пользователи, привлеченные на данном канале, едва окупились.

    Рекомендации:

  • Увеличить фокус внимания на удержании пользователей. Особенно самой крупной когорты из США.
  • Стоит обратить внимание на продукт и рекламу для клиентов Mac и iPhone. У них высокая конверсия, но низкое удержание.
  • 80% рекламного бюджета тратится на привлечение неокупаемых пользователей через TipTop, FaceBoom. Стоит оценить целесообразность дальнейшего сотрудничества с данными каналами. Пользователей с FaceBoom мы не можем удержать, а цены TipTop непомерно высоки. RocketSuperAds выглядит перспективным для расширения использования.
  • Стоит дополнительно изучить аудиторию AdNonSence: почему она хорошо конвертируется и плохо удерживается.
  • Возможно проблемы с удержанием клиентов FaceBoom и AdNonSence имеют похожие корни. Возможно это клиенты, чье ожидание не соответствуют нашему продукту? Или в рекламе на этом канале мы даем неправильное понятие о содержании нашего продукта? Или это неплатежеспособная аудитория?
  • Канал WahooNetBanner требует внимания: только на территории Англии ROI снижается.

P.S.¶

Я провела дополнительное исследование. Посмотрим что происходит с окупаемостью рекламы в каждом конкретном регионе. Для этого я объединила в новую функцию LTV, ROI, CAC, конверсию и удержание. Исключила органических пользователей (для всех стран кроме Англии, в Англии до 14 дня доживают только органические пользователи).

Данное дополнительное исследование позволила найти некоторые особенности (например, что каналы привлечения четко поделены на США и Европейский рынок, что внутри Европейского рынка поведение клиентов на одних и тех же каналах неодинаково - этого не видно из анализа в п.6.1.3 так как внутри европейских каналов привлечения сидят клиенты из трех стран).

In [44]:
def analytic_by_contries(country, dimensions):
    '''Анализируем окупаемость рекламы c помощью графиков LTV и ROI, 
    а также графиков динамики LTV, CAC и ROI. 
    Исключаем из анализа органических пользователей.
    Строим графики конверсии и удержания.
    '''
    # смотрим окупаемость с разбивкой по выбранному критерию
    dimensions = [dimensions]
    ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
        profiles.query('(channel != "organic") & (region == @country)'), 
        orders, observation_date, horizon_days, dimensions=dimensions)
    plot_ltv_roi(
        ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=20)
    
    # смотрим конверсию
    conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles.query('(channel != "organic") & (region == @country)'), 
    orders, observation_date, horizon_days, dimensions=dimensions)
    plot_conversion(conversion_grouped, conversion_history, horizon_days)
    
    # смотрим удержание 
    retention_raw, retention_grouped, retention_history = get_retention(
        profiles.query('(channel != "organic") & (region == @country)'), 
        visits, observation_date, horizon_days, dimensions= dimensions)
    plot_retention(retention_grouped, retention_history, horizon_days)
In [45]:
def analytic_by_contries1(country, dimensions):
    '''Анализируем окупаемость рекламы c помощью графиков LTV и ROI, 
    а также графиков динамики LTV, CAC и ROI. 
    Исключаем из анализа органических пользователей.
    Строим графики конверсии и удержания.
    '''
    # смотрим окупаемость с разбивкой по выбранному критерию
    dimensions = [dimensions]
    ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
        profiles.query('(region == @country)'), 
        orders, observation_date, horizon_days, dimensions=dimensions)
    plot_ltv_roi(
        ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=20)
    
    # смотрим конверсию
    conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles.query('(region == @country)'), 
    orders, observation_date, horizon_days, dimensions=dimensions)
    plot_conversion(conversion_grouped, conversion_history, horizon_days)
    
    # смотрим удержание 
    retention_raw, retention_grouped, retention_history = get_retention(
        profiles.query('(region == @country)'), 
        visits, observation_date, horizon_days, dimensions= dimensions)
    plot_retention(retention_grouped, retention_history, horizon_days)

Пользователи из США.¶

Рассмотрим более подробно пользователей из США. Так как только они не окупаются, насколько каналы привлечения и устройства влияют на это.

In [46]:
analytic_by_contries('United States', 'channel')
  • В США мы привлекаем пользователей через 5 каналов: TipTop, FaceBoom, YRabbit, MediaTornado и RocketSuperAds.
  • Пользователи, привлеченные на каналах TipTop и FaceBoom стабильно не окупаются.
  • Вероятно стоит увеличить предвижение через RocketSuperAds, так как конверсия в платящих пользователей у этого канала достаточно высока 7.91%, стоимость привлечения для США невелика 0.5-0.3 доллара, а ROI стабилен для пользователей, привлеченных в мае-августе, с тенденцией к увеличению для пользователей, привлеченных в сентябре-октябре.

Пользователи с FaceBoom очень хорошо конвертируются и очень плохо удерживаются. Пользователи с TipTop хорошо конвертируются, удержания их не хуже друих каналов, но не окупаются.

In [47]:
analytic_by_contries('United States', 'device')

Тип устройства не влияет на окупаемость пользователей из США.

Пользователи Android в США конвертируются чуть лучше остальных. Удержание не зависит от устройства.

Пользователи из Англии.¶

Рассмотрим более подробно пользователей из Англии, они окупаются лучше остальных. Насколько каналы привлечения и устройства влияют на это. Можно ли применить этот опыт на пользователях из других стран.

In [48]:
analytic_by_contries1('UK', 'channel')
In [49]:
# посчитаем месячный рекламный бюджет в разрезе каналов
market_budget = profiles.query('(channel != "organic") & (region == "UK")')\
                         .pivot_table(index='channel', 
                                      columns='month', 
                                      values='acquisition_cost', 
                                      aggfunc='sum')
market_budget['sum'] = market_budget.sum(axis=1)
market_budget['sum_%'] = market_budget['sum'] / market_budget['sum'].sum() * 100

market_budget
Out[49]:
month 2019-05-01 00:00:00 2019-06-01 00:00:00 2019-07-01 00:00:00 2019-08-01 00:00:00 2019-09-01 00:00:00 2019-10-01 00:00:00 sum sum_%
channel
AdNonSense 402.58 179.65 172.80 177.29 193.68 179.64 1305.65 25.76
LeapBob 51.04 108.82 118.91 130.27 124.54 106.03 639.61 12.62
OppleCreativeMedia 63.02 128.42 128.72 154.08 153.45 135.49 763.18 15.06
WahooNetBanner 147.67 321.11 290.46 361.45 338.68 349.52 1808.89 35.69
lambdaMediaAds 164.38 75.20 74.80 74.95 89.28 72.57 551.17 10.87
  • В Англии мы используем AdNonSense, LeapBob, OppleCreativeMedia, WahooNetBanner и lambdaMediaAds для привлечения пользователей.
  • Пользователи, пришедшие из AdNonSense (несмотря на высокую конверсию пользователей с данного канала 11.34%), стабильно не окупаются за 14 дней. Доля AdNonSense в рекламном бюджете по Англии 26%.
  • WahooNetBanner имеет 36% в структуре рекламных расходов по Англии. Последние два месяца пользователи, привлеченные на данном канале, едва окупились.
  • Лучше всего окупаются пользователи с канала lambdaMediaAds. Но пользователи с этого канала приходят не регулярно.
  • Каналы LeapBob и OppleCreativeMedia показывают наиболее стабильные результаты: минимальный САС на уровне 2,5 долларов и регулярный ROI >1.

Пользователи AdNonSens конвертируются лучше всех. Но их удержание в 2 раза хуже, чем с остальных каналов. Возможно это не наши пользователи?

In [50]:
analytic_by_contries1('UK', 'device')
  • В Англии за 14 дней окупаются пользователи со всех устройств.
  • САС пользователей не зависит от устройств.
  • ROI пользователей Mac, привлеченныхв июле, и пользователей iPhone, привлеченных в сентябре, ниже 1. Возможно у нас нерегулярные проблемы с системой iOS.

В Англии лучше всего конвертируются пользователи РС, хуже всего - пользователи Mac.

Пользователи из Франции.¶

In [51]:
analytic_by_contries('France', 'channel')

Пользователи, привлеченные в AdNonSence, не окупаются и для Франции. При этом они хорошо конвертируются и плохо удерживаются.

In [52]:
analytic_by_contries('France', 'device')

Во Франции лучше всего окупаются пользователи Мас, хуже всего - Android.

Пользователи из Германии.¶

In [53]:
analytic_by_contries('Germany', 'channel')

Пользователи, привлеченные в AdNonSence, не окупаются и в Германии. При этом они хорошо конвертируются и плохо удерживаются.

In [54]:
analytic_by_contries('Germany', 'device')

В Германии лучше всего конвертируются пользователи PC и Android. Окупаются пользователи всех устройств, но хуже всего пользователи Мас.

Дополнительные выводы.¶

Окупаемость рекламных вложений не зависит от типа устройства.

  • В США не окупаются пользователи всех устройств, в Европейском регионе - окупаются.
  • У пользователей РС самые высокие показатели ROI и конверсии в Англии и Германии.
  • В США пользователи Android конвертируются чуть лучше остальных.

В Англии ROI пользователей Mac, привлеченных в июле, и пользователей iPhone, привлеченных в сентябре, ниже 1. Возможно у нас нерегулярные проблемы с системой iOS.

Рекламные каналы разделены по региональному признаку.

  • В США мы привлекаем пользователей через TipTop, FaceBoom, YRabbit, MediaTornado и RocketSuperAds.

  • В странах Европейского региона мы используем AdNonSense, LeapBob, OppleCreativeMedia, WahooNetBanner и lambdaMediaAds.

    • WahooNetBanner имеет 36% в структуре рекламных расходов по Англии. Последние два месяца пользователи, привлеченные на данном канале, едва окупились.

Канал WahooNetBanner требует внимания: только на территории Англии ROI снижается.